Skip to content

introduce offer mints and accounts#959

Open
gudnuf wants to merge 1 commit intomasterfrom
add-offer-accounts
Open

introduce offer mints and accounts#959
gudnuf wants to merge 1 commit intomasterfrom
add-offer-accounts

Conversation

@gudnuf
Copy link
Copy Markdown
Contributor

@gudnuf gudnuf commented Mar 25, 2026

Replace closed_loop boolean with a purpose enum (transactional | gift-card | offer) in mint info extensions. Offer mints have NUT-04 minting disabled and keyset-based expiry.

I will open follow up PRs that add the account state (active, expired, deleted), and one for the offers UI

Replace closed_loop boolean with a purpose enum (transactional | gift-card | offer)
in mint info extensions. Offer mints have NUT-04 minting disabled and keyset-based
expiry for promotional ecash distribution.

- Add 'offer' to MintPurpose type and account_purpose DB enum
- Add expiresAt field to CashuAccount (derived from active keyset final_expiry)
- Allow NUT-04 disabled in mint validation for offer mints
- Adapt canSendToLightning/canReceiveFromLightning for 3-purpose model
- Update isTestMintQueryOptions to handle mints with minting disabled
@gudnuf gudnuf requested a review from jbojcic1 March 25, 2026 23:14
@gudnuf gudnuf self-assigned this Mar 25, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agicash Ready Ready Preview, Comment Mar 25, 2026 11:16pm

Request Review

@supabase
Copy link
Copy Markdown

supabase bot commented Mar 25, 2026

Updates to Preview Branch (add-offer-accounts) ↗︎

Deployments Status Updated
Database Wed, 25 Mar 2026 23:15:43 UTC
Services Wed, 25 Mar 2026 23:15:43 UTC
APIs Wed, 25 Mar 2026 23:15:43 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations ⚠️ Wed, 25 Mar 2026 23:19:05 UTC
Migrations Wed, 25 Mar 2026 23:19:09 UTC
Seeding Wed, 25 Mar 2026 23:19:09 UTC
Edge Functions Wed, 25 Mar 2026 23:19:09 UTC

⚠️ Warning — Service health check failed


View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

* Converted from the active keyset's `final_expiry` unix epoch (NUT-02).
* Null for non-offer accounts or when the keyset has no expiry.
*/
expires_at: z.string().nullable().optional(),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at this plan for adding account state here I'm wondering if expires_at should be a new column, not in the cashu account jsonb.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should. See my comment here. Do you see some benefits of it being in cashu jsonb?

@orveth orveth mentioned this pull request Mar 26, 2026
7 tasks
@orveth orveth requested review from ditto-agent and removed request for jbojcic1 March 28, 2026 09:20
@gudnuf gudnuf changed the title feat: add offer mint purpose to cashu protocol extensions introduce offer mints and accounts Mar 28, 2026
* The purpose of a Cashu mint as advertised in its info response.
* - 'transactional': Regular mint for sending/receiving payments
* - 'gift-card': Closed-loop mint issuing gift cards
* - 'offer': Promotional ecash with an expiry
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit but should we mention that this one is closed loop too?

return false;
}
const wallet = getCashuWallet(mintUrl);
const { request: bolt11 } = await wallet.createMintQuoteBolt11(1);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we just remove this check since we discussed how it doesn't work anyway? maybe we should just consider it a test mint if included in knownTestMints

* Assumes one active keyset per unit on offer mints.
* @returns The ISO 8601 timestamp when the offer expires, or null if the keyset has no expiry.
*/
export const getOfferExpiresAt = (
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably have this function return Date | null and then the caller can convert to iso string if needed

Also I think this function should be called getKeysetExpiry or getMintExpiry or something like that since it doesn't do anything purpose specific.

* plus 'transactional' which also applies to non-cashu account types (e.g. Spark).
*/
export type AccountPurpose = 'transactional' | 'gift-card';
export type AccountPurpose = 'transactional' | MintPurpose;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but transactional already exists on MintPurpose so should be enough to do export type AccountPurpose = MintPurpose

* Converted from the active keyset's `final_expiry` unix epoch (NUT-02).
* Null for non-offer accounts.
*/
expiresAt: string | null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if account expiry should be top level account concept and not cashu specific thing. Similar to how purpose is atm even though for Spark it is always transactional atm.

/**
* Returns true if the account can send payments through the Lightning network.
* Returns false for test mints and gift-card accounts.
* Returns false for offline wallets, test mints, non-transactional accounts,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did we change this to check online status too now? if we are doing that shouldn't we check only status for spark type too?

* Converted from the active keyset's `final_expiry` unix epoch (NUT-02).
* Null for non-offer accounts or when the keyset has no expiry.
*/
expires_at: z.string().nullable().optional(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should. See my comment here. Do you see some benefits of it being in cashu jsonb?

currency: Currency,
): string | null => {
const unit = getCashuProtocolUnit(currency);
const activeKeyset = keysets.find((ks) => ks.unit === unit && ks.active);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can there be more than one active keyset for the same unit?

mintInfo: ExtendedMintInfo,
) => {
return queryOptions({
queryKey: ['is-test-mint', mintUrl, mintInfo.isSupported(4).disabled],
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this change for?

| 'isOnline'
>;
}) {
const isTestMint = await checkIsTestMint(account.mintUrl);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't this be a simple fn call anymore?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants